カーネル法を用いたサポートベクターマシン(Support Vector Machine with Kernel, SVM)
Overview
カーネル法を用いたサポートベクターマシン(ただのSVMとも呼ばれる)は、より複雑なモデルを可能にするために線形サポートベクターマシンを拡張したものです。サポートベクターマシンは、クラス分類にも回帰にも利用できますが、ここではSVCとして実装されているクラス分類についてだけまとめます。
Background
線形モデルによるクラス分類では、直線で分離することしかできないので、下のようなデータセットではうまくいきません。
code: Python
import matplotlib.pyplot as plt
import mglearn
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
from sklearn.svm import LinearSVC
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
linear_svc = LinearSVC().fit(X, y)
mglearn.plots.plot_2d_separator(linear_svc, X)
mglearn.discrete_scatter(X:, 0, X:, 1, y) plt.xlabel('Feature 0')
plt.ylabel('Feature 1')
plt.show()
https://gyazo.com/114506738208ee410c74acb53d9db4c0
そこで、入力特徴量を拡張します。例えば、feature1 ** 2、つまり2番目の特徴量の2乗を新しい特徴量として加えてみます。これでデータポイントは(feature0, feature1)の2次元の点ではなく、(feature0, feature1, feature1 ** 2)の3次元の点になります。新しい表現を3次元散布図にしたものを次に示します。
code: Python
import numpy as np
import matplotlib.pyplot as plt
import mglearn
from sklearn.datasets import make_blob
from mpl_toolkits.mplot3d import Axes3D, axes3d
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
# 2番目の特徴量の2乗を追加
X_new = np.hstack([X, X:, 1: ** 2]) # 3Dで可視化
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
# y == 0の点をプロットしてからy == 1の点をプロット
mask = y == 0
ax.set_xlabel('Feature 0')
ax.set_ylabel('Feature 1')
ax.set_zlabel('Feature1 ** 2')
plt.show()
https://gyazo.com/c42201fa4537868a704b7ec5a98d15be
この新しい表現では、2つのクラスを3次元空間内の平面を用いて分類することが可能になっています。
code: Python
linear_svm_3d = LinearSVC().fit(X_new, y)
coef, intercept = linear_svm_3d.coef_.ravel(), linear_svm_3d.intercept_
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
xx = np.linspace(X_new:, 0.min() - 2, X_new:, 0.max() + 2, 50) yy = np.linspace(X_new:, 1.min() - 2, X_new:, 1.max() + 2, 50) XX, YY = np.meshgrid(xx, yy)
ZZ = (coef0 * XX + coef1 * YY + intercept) / -coef2 ax.plot_surface(XX, YY, ZZ, rstride=8, cstride=8, alpha=.3)
ax.set_xlabel('Feature 0')
ax.set_ylabel('Feature 1')
ax.set_zlabel('Feature1 ** 2')
plt.show()
https://gyazo.com/738f2b3eb2b70bdf9b84ef00f97a357f
最初の特徴量の関数としてみると、線形SVMモデルは線形ではなくなっています。次の図からわかるように楕円に近くなっています。
code: Python
ZZ = YY ** 2
cmap=mglearn.cm2, alpha=.5)
mglearn.discrete_scatter(X:, 0, X:, 1, y) plt.xlabel('Feature 0')
plt.ylabel('Feature 1')
plt.show()
https://gyazo.com/478a49d0ae117ae870230aab5a44d2b6
Theory
カーネルトリック
上の例では特徴量1の2乗を新しい特徴量として加えることでより強力なモデルとなりましたが、通常、どんな特徴量を追加すれば良いのかはわかりません。カーネルトリック(kernel trick)はこの問題に対応する手法です。 カーネルトリックを使うと実際に特徴量を拡張することなしに、拡張後の空間での特徴量を直接計算できます。
サポートベクターマシンで広く用いられている高次元空間へのマップ方法が2つあります。もとの特徴量の特定の次数までのすべての多項式(feature1 ** 2 * feature2 ** 5など)を計算する多項式カーネル(Polynomial Kernel)と、放射基底関数(Radial Basis Function, RBF)カーネルとも呼ばれるガウシアンカーネルです。この辺りの数学的説明はとても難しいです。
サポートベクタ(support vector)
学習の過程で、SVMは個々のデータポイントが、2つのクラスの決定境界を表現するのにどの程度重要かを学習します。多くの場合、2つのクラスの境界に位置するごく一部の訓練データポイントだけが決定境界を決定する。これらのデータポイントのことをサポートベクタと呼びます。これがサポートベクターマシンの名前の由来です。
新しいデータポイントに対して予測を行う際に、サポートベクタとデータポイント間の距離が測定されます。クラス分類は、このサポートベクタとの距離と、学習過程で学習された個々のサポートベクタの重要性によって決定されます。データポイント間の距離は次のように定義されるガウシアンカーネルで測られます。
ガウシアンカーネル
$ k_{rbf}(x_1, x_2) = exp(-\gamma||x_1 - x_2||^2)
ここで、$ x_1と$ x_2はデータポイントです。$ ||x_1 - x_2||^2はユークリッド距離を表し、$ \gammaはガウシアンカーネルの幅を制御するパラメータです。
Coding(Classification)
handcraftedデータセットでガウシアンカーネルを用いてモデルを構築・学習・評価する
code: Python
import matplotlib.pyplot as plt
import mglearn
from sklearn.svm import SVC
X, y = mglearn.tools.make_handcrafted_dataset()
svm = SVC(kernel='rbf', C=10, gamma=0.1).fit(X, y)
mglearn.plots.plot_2d_separator(svm, X, eps=.5)
mglearn.discrete_scatter(X:, 0, X:, 1, y) # サポートベクタをプロット
sv = svm.support_vectors_
# サポートベクターのクラスラベルはdual_coef_の正負によって与えられる
sv_labels = svm.dual_coef_.ravel() > 0
mglearn.discrete_scatter(sv:, 0, sv:, 1, sv_labels, s=15, markeredgewidth=3) plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
plt.show()
https://gyazo.com/78220fa170bf850ffedf8a17c8e03b25
この場合、SVMによる境界は非常に滑らかで、非線形です。ここでは、Cとgammaの2つのパラメータを調整しています。gammaパラメータは、ガウシアンカーネルの幅を調整します。このパラメータが、点が近いということを意味するスケールを決定します。Cパラメータは正則化パラメータです。
SVMパラメータCとgammaの調整を可視化する
code: Python
fig, axes = plt.subplots(3, 3, figsize=(15, 10))
for a, gamma in zip(ax, range(-1, 2)):
mglearn.plots.plot_svm(log_C=C, log_gamma=gamma, ax=a)
plt.show()
https://gyazo.com/d447eaf07529ad5fef33aefce054bdec
gammaが小さいと、ガウシアンカーネルの直径が大きくなり、多くの点を近いと判断するようになるのでモデルの複雑さは小さくなります。また、Cが小さいと、正則化が強まるのでモデルの複雑さは小さくなります。
Advanced
cancerデータセットでガウシアンカーネルを用いてモデルを構築・学習・評価する
code: Python
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.svm import SVC
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
# デフォルトパラメータは、C=1、gamma=1/n_features
svc = SVC().fit(X_train, y_train)
print('Accuracy on traning set: {:.2f}'.format(svc.score(X_train, y_train)))
print('Accuracy on test set: {:.2f}'.format(svc.score(X_test, y_test)))
--------------------------------------------------------------------------
Accuracy on traning set: 1.00
Accuracy on test set: 0.63
--------------------------------------------------------------------------
これは、強く過剰適合しています。SVMはうまくいく場合が多いですが、パラメータの設定と、データのスケールに敏感であるという問題があります。特に、すべての特徴量の変位が同じスケールであることを要求します。そこで、一度cancerデータセットの特徴量の最大値と最小値を、対数で可視化したものを見てみます。
cancerデータセットの特徴量の最大値と最小値を対数で可視化する
code: Python
plt.plot(X_train.min(axis=0), 'o', label='min')
plt.plot(X_train.max(axis=0), '^', label='max')
plt.legend(loc=4)
plt.xlabel('Feature index')
plt.ylabel('Feature magnitude')
plt.yscale('log')
plt.show()
https://gyazo.com/1c7f759f6c546ece2d47f240350ce0d2
cancerデータセットの特徴量は相互に桁違いにサイズが違うことがわかります。これは他のモデルであっても問題になりますが、カーネル法を用いたSVMでは破壊的な影響をもたらします。そこで、データの前処理を行う必要があります。
SVCのためのデータ前処理
すべての特徴量が大体同じスケールになるように、それぞれスケール変換を行います。カーネル法を用いたSVMでよく使われる方法は、すべての特徴量が0から1の間になるような、MinMaxScalerと呼ばれる方法です。
code: Python
# 訓練セットの特徴量ごとに最小値を計算
min_on_training = X_train.min(axis=0)
# 訓練セットの特徴量ごとにレンジ(最大値 - 最小値)を計算
range_on_training = (X_train - min_on_training).max(axis=0)
# 最小値を引いてからレンジで割ることで、min=0、max=1に変換される
X_train_scaled = (X_train - min_on_training) / range_on_training
print('Minimum for each feature\n: {}'.format(X_train_scaled.min(axis=0)))
print('Maximum for each feature\n: {}'.format(X_train_scaled.max(axis=0)))
# テストセットに対しても、まったく同じ変換を行う
# 訓練セットの最小値とレンジを用いる
X_test_scaled = (X_test - min_on_training) / range_on_training
svc = SVC().fit(X_train_scaled, y_train)
print('Accuracy on training set: {:.3f}'.format(svc.score(X_train_scaled, y_train)))
print('Accuracy on test set: {:.3f}'.format(svc.score(X_test_scaled, y_test)))
--------------------------------------------------------------------------
Minimum for each feature
: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0.]
Maximum for each feature
: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
1. 1. 1. 1. 1. 1.]
Accuracy on training set: 0.948
Accuracy on test set: 0.951
--------------------------------------------------------------------------
データをスケール変換したら結果が一変しました。
Summary
Merit
様々なデータセットに対してうまく機能する
データにわずかな特徴量しかない場合にも複雑な決定境界を生成できる
Demerit
サンプルの個数が大きくなるとうまく機能しない(100,000サンプルぐらいから)
データ前処理とパラメータの調整が大変 → そのため、勾配ブースティングなどの決定木ベースのモデルのほうが広く使われている
予測された理由を理解したり説明したりするのが難しい
Parameters
gamma
大きくするほどより複雑なモデルになる
C
大きくするほどより複雑なモデルになる